/*
    SDL - Simple DirectMedia Layer
    Partial implementation of SDL library (originally written by
    Sam Lantinga <slouken@libsdl.org>) for the particular purpose to support
    Prequengine (http://code.google.com/p/prequengine/) on BlackBerry(tm)
    tablets and smartphones.

    Copyright (C) 2013  xlamsp

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

    xlamsp@gmail.com
*/

#include <pthread.h>
#include <sys/asoundlib.h>
#include "SDL_timer.h"
#include "SDL_audio.h"
#include "SDL_mixer.h"
#include "load_voc.h"


/* Magic numbers for various audio file formats */
#define RIFF    0x46464952      /* "RIFF" */
#define WAVE    0x45564157      /* "WAVE" */
#define FORM    0x4d524f46      /* "FORM" */
#define OGGS    0x5367674f      /* "OggS" */
#define CREA    0x61657243      /* "Crea" */
#define FLAC    0x43614C66      /* "fLaC" */

struct _Mix_Channel {
    Mix_Chunk *chunk;
    int playing;
    int paused;
    Uint8 *samples;
    int volume;
    int looping;
    int distance;
};

typedef struct {
    int audio_opened;
    SDL_AudioSpec spec;
    int num_channels;
    int reserved_channels;
    struct _Mix_Channel *mix_channel;

    snd_pcm_t *pcm;
    pthread_mutex_t mutex;
    pthread_t thread;
    int thread_quit;
} mixer_t;

static mixer_t mixer;

/* Music function declarations */
extern int open_music(SDL_AudioSpec *mixer);
extern void close_music(void);

/* Support for user defined music functions, plus the default one */
extern int volatile music_active;
extern void music_mixer(void *udata, Uint8 *stream, int len);
static void (*mix_music)(void *udata, Uint8 *stream, int len) = music_mixer;
static void *music_data = NULL;


static void *Mix_DoEffects(int chan, void *snd, int len)
{
    #warning: stub function, not implemented yet!
    return snd;
}

static void sound_quit(mixer_t *mixer)
{
    SDL_Log(DEBUG1, "entered, mixer=0x%p", mixer);

    if(mixer) {
        if(mixer->pcm) {
            snd_pcm_close(mixer->pcm);
            mixer->pcm = NULL;
        }
        if(mixer->spec.userdata) {
            SDL_free(mixer->spec.userdata);
            mixer->spec.userdata = NULL;
        }
    }
}

static int sound_init(mixer_t *mixer, const char *name)
{
    int ret;
    char *dev_name;
    snd_pcm_t *pcm;
    int format;
    Uint8 *buffer;
    snd_pcm_channel_info_t ci;
    snd_pcm_channel_params_t pcp;

    SDL_Log(DEBUG1, "entered, mixer=0x%p", mixer);

    if(mixer->pcm) {
        sound_quit(mixer);
    }

    if(name == NULL) {
        dev_name = "pcmPreferred";
    } else {
        dev_name = (char*)name;
    }

    switch (mixer->spec.format) {
        case AUDIO_U8:
            format = SND_PCM_SFMT_U8;
            break;

        case AUDIO_S8:
            format = SND_PCM_SFMT_S8;
            break;

        case AUDIO_U16LSB:
            format = SND_PCM_SFMT_U16_LE;
            break;

        case AUDIO_S16LSB:
            format = SND_PCM_SFMT_S16_LE;
            break;

        case AUDIO_U16MSB:
            format = SND_PCM_SFMT_U16_BE;
            break;

        case AUDIO_S16MSB:
            format = SND_PCM_SFMT_S16_BE;
            break;

        default:
            SDL_Log(ERROR, "unsupported format: 0x%x", (Uint32)mixer->spec.format);
            return -1;
    }

    ret = snd_pcm_open_name(&pcm, dev_name, SND_PCM_OPEN_PLAYBACK);
    if(ret) {
        SDL_Log(ERROR, "snd_pcm_open_name() failed, %d (%s)", ret, snd_strerror(ret));
        return -1;
    }

    ret = snd_pcm_plugin_set_disable(pcm, PLUGIN_DISABLE_MMAP);
    if(ret < 0) {
        SDL_Log(WARNING, "snd_pcm_plugin_set_disable() failed, %d (%s)", ret, snd_strerror(ret));
    }

    memset(&ci, 0, sizeof(ci));
    ci.channel = SND_PCM_CHANNEL_PLAYBACK;
    ret = snd_pcm_plugin_info(pcm, &ci);
    if(ret < 0) {
        SDL_Log(ERROR, "snd_pcm_plugin_info() failed %d (%s)", ret, snd_strerror(ret));
        snd_pcm_close(pcm);
        return -1;
    }

    buffer = (Uint8*)SDL_malloc(mixer->spec.size);
    if(!buffer) {
        SDL_Log(ERROR, "Out of memory (%u)", mixer->spec.size);
        snd_pcm_close(pcm);
        return -1;
    }

    memset(&pcp, 0, sizeof(pcp));
    pcp.mode = SND_PCM_MODE_BLOCK;
    pcp.channel = SND_PCM_CHANNEL_PLAYBACK;
    pcp.start_mode = SND_PCM_START_FULL;
    pcp.stop_mode = SND_PCM_STOP_STOP;

    pcp.buf.block.frag_size = mixer->spec.size;
    pcp.buf.block.frags_min = 1;
    pcp.buf.block.frags_max = 4;

    pcp.format.interleave = 1;
    pcp.format.format = format;
    pcp.format.voices = mixer->spec.channels;
    pcp.format.rate = mixer->spec.freq;

    sprintf(pcp.sw_mixer_subchn_name, "pcm=0x%p", pcm);

    ret = snd_pcm_plugin_params(pcm, &pcp);
    if(ret < 0) {
        SDL_Log(ERROR, "snd_pcm_plugin_params() failed %d (%s)", ret, snd_strerror(ret));
        SDL_free(buffer);
        snd_pcm_close(pcm);
        return -1;
    }

    ret = snd_pcm_plugin_prepare(pcm, SND_PCM_CHANNEL_PLAYBACK);
    if(ret < 0) {
        SDL_Log(ERROR, "snd_pcm_plugin_prepare failed %d (%s)", ret, snd_strerror(ret));
        SDL_free(buffer);
        snd_pcm_close(pcm);
        return -1;
    }

    mixer->pcm = pcm;
    mixer->spec.userdata = buffer;

    return 0;
}

/* Mixing function */
static void mix_channels(void *udata, Uint8 *stream, int len)
{
    Uint8 *mix_input;
    int i, mixable, volume = SDL_MIX_MAX_VOLUME;

    memset(stream, mixer.spec.silence, len);

    /* Mix the music (must be done before the channels are added) */
    if(music_active) {
        mix_music(music_data, stream, len);
    }

    /* Mix any playing channels... */
    for(i = 0; i < mixer.num_channels; i++) {
        if(!mixer.mix_channel[i].paused) {
            if(mixer.mix_channel[i].playing > 0) {
                int index = 0;
                int remaining = len;
                while(mixer.mix_channel[i].playing > 0 && index < len) {
                    remaining = len - index;
                    volume = (mixer.mix_channel[i].volume * mixer.mix_channel[i].chunk->volume) / SDL_MIX_MAX_VOLUME;
                    volume = (volume * (256 - mixer.mix_channel[i].distance)) / 256;
                    mixable = mixer.mix_channel[i].playing;
                    if(mixable > remaining) {
                        mixable = remaining;
                    }

                    mix_input = Mix_DoEffects(i, mixer.mix_channel[i].samples, mixable);
                    SDL_MixAudio(stream + index, mix_input, mixable, volume);
                    if(mix_input != mixer.mix_channel[i].samples) {
                        SDL_free(mix_input);
                    }

                    mixer.mix_channel[i].samples += mixable;
                    mixer.mix_channel[i].playing -= mixable;
                    index += mixable;
                }

                /* If looping the sample and we are at its end, make sure
                   we will still return a full buffer */
                while(mixer.mix_channel[i].looping && index < len ) {
                    int alen = mixer.mix_channel[i].chunk->alen;
                    remaining = len - index;
                    if(remaining > alen) {
                        remaining = alen;
                    }

                    mix_input = Mix_DoEffects(i, mixer.mix_channel[i].chunk->abuf, remaining);
                    SDL_MixAudio(stream + index, mix_input, remaining, volume);
                    if(mix_input != mixer.mix_channel[i].chunk->abuf) {
                        SDL_free(mix_input);
                    }

                    mixer.mix_channel[i].looping--;
                    mixer.mix_channel[i].samples = mixer.mix_channel[i].chunk->abuf + remaining;
                    mixer.mix_channel[i].playing = mixer.mix_channel[i].chunk->alen - remaining;
                    index += remaining;
                }
                if(!mixer.mix_channel[i].playing && mixer.mix_channel[i].looping) {
                    mixer.mix_channel[i].looping--;
                    mixer.mix_channel[i].samples = mixer.mix_channel[i].chunk->abuf;
                    mixer.mix_channel[i].playing = mixer.mix_channel[i].chunk->alen;
                }
            }
        }
    } /* for */
}

/* Mixer thread */
static void *mixer_thread(void *arg)
{
    mixer_t *mi = (mixer_t*)arg;
    SDL_AudioSpec *spec = &(mi->spec);
    SDL_Log(DEBUG1, "entered");

    sound_init(mi, NULL);

    while(!mi->thread_quit) {
        pthread_mutex_lock(&(mi->mutex));
        spec->callback(mi, mi->spec.userdata, mi->spec.size);
        pthread_mutex_unlock(&(mi->mutex));

        int written = 0;
        snd_pcm_channel_status_t status;

        written = snd_pcm_plugin_write(mi->pcm, mi->spec.userdata, mi->spec.size);
        if (written < mi->spec.size) {
            memset(&status, 0, sizeof(status));
            status.channel = SND_PCM_CHANNEL_PLAYBACK;
            if (snd_pcm_plugin_status(mi->pcm, &status) < 0) {
                SDL_Log(ERROR, "failed snd_pcm_plugin_status()");
                break;
            }
            SDL_Log(DEBUG1, "written=%d < size=%u status=%d", written, mi->spec.size, status.status);

            if (status.status == SND_PCM_STATUS_READY ||
                status.status == SND_PCM_STATUS_UNDERRUN) {
                if (snd_pcm_plugin_prepare(mi->pcm, SND_PCM_CHANNEL_PLAYBACK) < 0) {
                    SDL_Log(ERROR, "failed snd_pcm_plugin_prepare()");
                    break;
                }
            }
            if (written < 0) {
                written = 0;
            }
            written += snd_pcm_plugin_write(mi->pcm, mi->spec.userdata + written, mi->spec.size - written);
        }
    }

    sound_quit(mi);

    SDL_Log(DEBUG1, "exit");
    return NULL;
}

void SDL_LockAudio(void)
{
    pthread_mutex_lock(&(mixer.mutex));
}

void SDL_UnlockAudio(void)
{
    pthread_mutex_unlock(&(mixer.mutex));
}

void SDL_CalculateAudioSpec(SDL_AudioSpec *spec)
{
    switch (spec->format) {
        case AUDIO_U8:
            spec->silence = 0x80;
            break;
        default:
            spec->silence = 0x00;
            break;
    }
    spec->size = (spec->format & 0xFF) / 8;
    spec->size *= spec->channels;
    spec->size *= spec->samples;
}

int Mix_OpenAudio(int frequency, Uint16 format, int nchannels, int chunksize)
{
    int i;

    nchannels = 1; /* force mono for Prequengine */

    SDL_Log(DEBUG1, "frequency=%d format=0x%x nchannels=%d chunksize=%d",
        frequency, (uint32_t)format, nchannels, chunksize);

    /* If the mixer is already opened, increment open count */
    if(mixer.audio_opened) {
        if(format == mixer.spec.format && nchannels == mixer.spec.channels) {
            mixer.audio_opened++;
            SDL_Log(DEBUG1, "already open, audio_opened=%d", mixer.audio_opened);
            return 0;
        }
        while(mixer.audio_opened) {
            Mix_CloseAudio();
        }
    }

    /* Verify some parameters */
    if(frequency == 0) {
        /* Pick some default audio frequency */
        frequency = 22050;
    }
    if(format == 0) {
        /* Pick some default audio format */
        format = AUDIO_S16;
    }
    if(nchannels == 0) {
        /* Pick a default number of channels */
        nchannels = 2;
    }
    switch (nchannels) {
        case 1:     /* Mono */
        case 2:     /* Stereo */
            break;

        default:
            SDL_Log(ERROR, "1 (mono) and 2 (stereo) channels supported");
            return -1;
    }
    if(chunksize == 0) {
        /* Pick a default of ~46 ms at desired frequency */
        int samples = (frequency / 1000) * 46;
        int power2 = 1;
        while(power2 < samples) {
            power2 *= 2;
        }
        chunksize = power2;
    }

    mixer.spec.freq = frequency;
    mixer.spec.format = format;
    mixer.spec.channels = nchannels;
    mixer.spec.samples = chunksize;
    mixer.spec.callback = mix_channels;
    mixer.spec.userdata = NULL;

    SDL_CalculateAudioSpec(&mixer.spec);

    /* Initialize the music players */
    if(open_music(&mixer.spec) < 0) {
        Mix_CloseAudio();
        return -1;
    }

    mixer.num_channels = MIX_CHANNELS;
    mixer.reserved_channels = 0;
    mixer.mix_channel = (struct _Mix_Channel*)SDL_calloc(mixer.num_channels, sizeof(struct _Mix_Channel));
    if(!mixer.mix_channel) {
        SDL_Log(ERROR, "failed to allocate %d bytes for mix_channel",
            mixer.num_channels * sizeof(struct _Mix_Channel));
        mixer.num_channels = 0;
        return -1;
    }

    /* Clear out the audio channels */
    for(i = 0; i < mixer.num_channels; i++) {
        mixer.mix_channel[i].volume = SDL_MIX_MAX_VOLUME;
    }
    Mix_VolumeMusic(SDL_MIX_MAX_VOLUME);

    if(pthread_mutex_init(&(mixer.mutex), NULL) == 0) {
        if(pthread_create(&(mixer.thread), NULL, mixer_thread, &mixer) == 0) {
            SDL_Log(DEBUG1, "success");
            mixer.audio_opened = 1;
            return 0;   /* sucess */
        }
        pthread_mutex_destroy(&(mixer.mutex));
    }
    SDL_free(mixer.mix_channel);
    mixer.mix_channel = NULL;
    mixer.num_channels = 0;

    SDL_Log(ERROR, "failed");
    return -1;
}

int Mix_AllocateChannels(int numchans)
{
    SDL_Log(DEBUG1, "numchans=%d", numchans);

    if(numchans < 0 || numchans == mixer.num_channels) {
        return mixer.num_channels;
    }

    if(numchans < mixer.num_channels) {
        /* Stop the affected channels */
        int i;
        for(i = numchans; i < mixer.num_channels; i++) {
            Mix_HaltChannel(i);
        }
    }

    SDL_LockAudio();
    struct _Mix_Channel *mc;
    mc = (struct _Mix_Channel*)SDL_realloc(mixer.mix_channel, numchans * sizeof(struct _Mix_Channel));
    if(!mc) {
        SDL_UnlockAudio();
        SDL_Log(WARNING, "failed to realloc %d bytes for mix_channel",
            numchans * sizeof(struct _Mix_Channel));
        return mixer.num_channels;
    }
    mixer.mix_channel = mc;

    if(numchans > mixer.num_channels) {
        /* Initialize the new channels */
        int i;
        for(i = mixer.num_channels; i < numchans; i++) {
            mixer.mix_channel[i].chunk = NULL;
            mixer.mix_channel[i].playing = 0;
            mixer.mix_channel[i].paused = 0;
            mixer.mix_channel[i].looping = 0;
            mixer.mix_channel[i].volume = SDL_MIX_MAX_VOLUME;
            mixer.mix_channel[i].distance = 0;
        }
    }
    mixer.num_channels = numchans;
    SDL_UnlockAudio();

    return numchans;
}

Mix_Chunk *Mix_LoadWAV_RW(SDL_RWops *src, int freesrc)
{
    SDL_Log(DEBUG1, "src=0x%p freesrc=%d", src, freesrc);

    Uint32 magic;
    Mix_Chunk *chunk;
    SDL_AudioSpec wavespec, *loaded;
    SDL_AudioCVT wavecvt;
    int samplesize;

    /* rcg06012001 Make sure src is valid */
    if(!src) {
        SDL_Log(ERROR, "Mix_LoadWAV_RW with NULL src");
        return NULL;
    }

    /* Make sure audio has been opened */
    if(!mixer.audio_opened) {
        SDL_Log(ERROR, "Audio device hasn't been opened");
        if(freesrc) {
            SDL_RWclose(src);
        }
        return NULL;
    }

    /* Allocate the chunk memory */
    chunk = (Mix_Chunk *)SDL_malloc(sizeof(Mix_Chunk));
    if(chunk == NULL) {
        SDL_Log(ERROR, "Out of memory");
        if(freesrc) {
            SDL_RWclose(src);
        }
        return NULL;
    }

    /* Find out what kind of audio file this is */
    magic = SDL_ReadLE32(src);
    /* Seek backwards for compatibility with older loaders */
    SDL_RWseek(src, -(int)sizeof(Uint32), RW_SEEK_CUR);

    switch (magic) {
#if 0 /* TODO */
        case WAVE:
        case RIFF:
            loaded = SDL_LoadWAV_RW(src, freesrc, &wavespec,
                    (Uint8 **)&chunk->abuf, &chunk->alen);
            break;
        case FORM:
            loaded = Mix_LoadAIFF_RW(src, freesrc, &wavespec,
                    (Uint8 **)&chunk->abuf, &chunk->alen);
            break;
#ifdef OGG_MUSIC
        case OGGS:
            loaded = Mix_LoadOGG_RW(src, freesrc, &wavespec,
                    (Uint8 **)&chunk->abuf, &chunk->alen);
            break;
#endif
#ifdef FLAC_MUSIC
        case FLAC:
            loaded = Mix_LoadFLAC_RW(src, freesrc, &wavespec,
                    (Uint8 **)&chunk->abuf, &chunk->alen);
            break;
#endif
#endif /* 0 */
        case CREA:
            loaded = Mix_LoadVOC_RW(src, freesrc, &wavespec,
                    (Uint8 **)&chunk->abuf, &chunk->alen);
            break;
        default:
            SDL_Log(ERROR, "Unrecognized sound file type: 0x%08x", magic);
            loaded = NULL;
            break;
    }
    if(!loaded) {
        SDL_Log(ERROR, "failed to load");
        SDL_free(chunk);
        if(freesrc) {
            SDL_RWclose(src);
        }
        return NULL;
    }

    /* Build the audio converter and create conversion buffers */
    if(SDL_BuildAudioCVT(&wavecvt,
            wavespec.format, wavespec.channels, wavespec.freq,
            mixer.spec.format, mixer.spec.channels, mixer.spec.freq) < 0 ) {
        SDL_Log(ERROR, "failed SDL_BuildAudioCVT()");
        SDL_free(chunk->abuf);
        SDL_free(chunk);
        return NULL;
    }
    samplesize = ((wavespec.format & 0xFF) / 8) * wavespec.channels;
    wavecvt.len = chunk->alen & ~(samplesize - 1);
    wavecvt.buf = (Uint8*)SDL_malloc(wavecvt.len * wavecvt.len_mult);
    if(wavecvt.buf == NULL) {
        SDL_Log(ERROR, "Out of memory");
        SDL_free(chunk->abuf);
        SDL_free(chunk);
        return NULL;
    }
    memcpy(wavecvt.buf, chunk->abuf, chunk->alen);
    SDL_free(chunk->abuf);

    /* Run the audio converter */
    if(SDL_ConvertAudio(&wavecvt) < 0) {
        SDL_Log(ERROR, "failed SDL_ConvertAudio()");
        SDL_free(wavecvt.buf);
        SDL_free(chunk);
        return(NULL);
    }
    chunk->allocated = 1;
    chunk->abuf = wavecvt.buf;
    chunk->alen = wavecvt.len_cvt;
    chunk->volume = SDL_MIX_MAX_VOLUME;

    return chunk;
}

void Mix_FreeChunk(Mix_Chunk *chunk)
{
    SDL_Log(DEBUG1, "chunk=0x%p", chunk);

    /* Caution -- if the chunk is playing, the mixer will crash */
    if(chunk) {
        /* Guarantee that this chunk isn't playing */
        SDL_LockAudio();
        if(mixer.mix_channel) {
            int i;
            for(i = 0; i < mixer.num_channels; i++) {
                if(chunk == mixer.mix_channel[i].chunk ) {
                    mixer.mix_channel[i].playing = 0;
                }
            }
        }
        SDL_UnlockAudio();
        /* Actually free the chunk */
        if(chunk->allocated) {
            SDL_free(chunk->abuf);
        }
        SDL_free(chunk);
    }
}

static int checkchunkintegral(Mix_Chunk *chunk)
{
    int frame_width = 1;

    if((mixer.spec.format & 0xFF) == 16) {
        frame_width = 2;
    }
    frame_width *= mixer.spec.channels;
    chunk->alen -= chunk->alen % frame_width;
    return chunk->alen;
}

/* Play an audio chunk on a specific channel.
   If the specified channel is -1, play on the first free channel.
   'ticks' is the number of milliseconds at most to play the sample, or -1
   if there is no limit.
   Returns which channel was used to play the sound.
*/
int Mix_PlayChannelTimed(int channel, Mix_Chunk *chunk, int loops, int ticks)
{
    int i;

    SDL_Log(DEBUG1, "channel=%d chunk=0x%p loops=%d ticks=%d",
        channel, chunk, loops, ticks);

    /* Don't play null pointers :-) */
    if(chunk == NULL) {
        SDL_Log(ERROR, "Tried to play a NULL chunk");
        return -1;
    }
    if(!checkchunkintegral(chunk)) {
        SDL_Log(ERROR, "Tried to play a chunk with a bad frame");
        return -1;
    }

    /* Lock the mixer while modifying the playing channels */
    SDL_LockAudio();
    {
        /* If channel is -1, play on the first free channel */
        if(channel == -1) {
            for(i = mixer.reserved_channels; i < mixer.num_channels; i++) {
                if(mixer.mix_channel[i].playing <= 0 )
                    break;
            }
            if(i == mixer.num_channels) {
                SDL_Log(ERROR, "No free channels available");
                channel = -1;
            } else {
                channel = i;
            }
        }

        /* Queue up the audio data for this channel */
        if(channel >= 0) {
            mixer.mix_channel[channel].samples = chunk->abuf;
            mixer.mix_channel[channel].playing = chunk->alen;
            mixer.mix_channel[channel].looping = loops;
            mixer.mix_channel[channel].chunk = chunk;
            mixer.mix_channel[channel].paused = 0;
        }
    }
    SDL_UnlockAudio();

    /* Return the channel on which the sound is being played */
    return channel;
}

int Mix_Volume(int channel, int volume)
{
    int i;
    int prev_volume;

    SDL_Log(DEBUG1, "channel=%d volume=%d", channel, volume);

    if(channel == -1) {
        prev_volume = 0;
        for(i = 0; i < mixer.num_channels; i++) {
            prev_volume += Mix_Volume(i, volume);
        }
        prev_volume /= mixer.num_channels;
    } else {
        prev_volume = mixer.mix_channel[channel].volume;
        if(volume >= 0) {
            if(volume > SDL_MIX_MAX_VOLUME) {
                volume = SDL_MIX_MAX_VOLUME;
            }
            mixer.mix_channel[channel].volume = volume;
        }
    }

    return prev_volume;
}

int Mix_HaltChannel(int channel)
{
    SDL_Log(DEBUG1, "channel=%d", channel);

    if(channel == -1) {
        int i;
        for(i = 0; i < mixer.num_channels; i++) {
            Mix_HaltChannel(i);
        }
    } else {
        SDL_LockAudio();
        if (mixer.mix_channel[channel].playing) {
            mixer.mix_channel[channel].playing = 0;
        }
        SDL_UnlockAudio();
    }

    return 0;
}

int Mix_Playing(int channel)
{
    int status = 0;

    SDL_Log(DEBUG2, "channel=%d", channel);

    if(channel == -1) {
        int i;
        for(i = 0; i < mixer.num_channels; i++) {
            if(mixer.mix_channel[i].playing > 0 || mixer.mix_channel[i].looping > 0) {
                status++;
            }
        }
    } else if(channel >= 0 && channel < mixer.num_channels) {
        if(mixer.mix_channel[channel].playing > 0 || mixer.mix_channel[channel].looping > 0) {
            status++;
        }
    }

    return status;
}

void Mix_CloseAudio(void)
{
    SDL_Log(DEBUG1, "entered, audio_opened=%d", mixer.audio_opened);

    if(mixer.audio_opened) {
        if(mixer.audio_opened == 1) {
            close_music();
            if(mixer.thread) {
                mixer.thread_quit = 1;
                pthread_join(mixer.thread, NULL);
                mixer.thread = 0;
                mixer.thread_quit = 0;
                pthread_mutex_destroy(&(mixer.mutex));
            }

            SDL_free(mixer.mix_channel);
            mixer.mix_channel = NULL;
            mixer.num_channels = 0;
            mixer.reserved_channels = 0;
        }
        mixer.audio_opened--;
    }

    return;
}

void Mix_Resume(int channel)
{
    SDL_Log(DEBUG1, "channel=%d", channel);

    SDL_LockAudio();
    if(channel == -1) {
        int i;

        for(i = 0; i < mixer.num_channels; i++) {
            if(mixer.mix_channel[i].playing > 0) {
                mixer.mix_channel[i].paused = 0;
            }
        }
    } else {
        if(mixer.mix_channel[channel].playing > 0 ) {
            mixer.mix_channel[channel].paused = 0;
        }
    }
    SDL_UnlockAudio();
}

int Mix_SetDistance(int channel, Uint8 distance)
{
    SDL_Log(DEBUG2, "channel=%d distance=%d", channel, distance);
    if(channel < 0 || channel >= mixer.num_channels || distance < 0 || distance >= 256) {
        return 0;
    }
    SDL_LockAudio();
    mixer.mix_channel[channel].distance = distance;
    SDL_UnlockAudio();

    return 1;
}


/* This table is used to add two sound values together and pin
 * the value to avoid overflow.  (used with permission from ARDI)
 * Changed to use 0xFE instead of 0xFF for better sound quality.
 */
static const Uint8 mix8[] =
{
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03,
  0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
  0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19,
  0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24,
  0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,
  0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A,
  0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45,
  0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50,
  0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B,
  0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
  0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71,
  0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C,
  0x7D, 0x7E, 0x7F, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
  0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x91, 0x92,
  0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D,
  0x9E, 0x9F, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8,
  0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB0, 0xB1, 0xB2, 0xB3,
  0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE,
  0xBF, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9,
  0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4,
  0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF,
  0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA,
  0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5,
  0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFE, 0xFE,
  0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE,
  0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE,
  0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE,
  0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE,
  0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE,
  0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE,
  0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE,
  0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE,
  0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE,
  0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE,
  0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE,
  0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE
};

/* The volume ranges from 0 - 128 */
#define ADJUST_VOLUME(s, v)     (s = (s * v) / SDL_MIX_MAX_VOLUME)
#define ADJUST_VOLUME_U8(s, v)  (s = (((s - 128) * v) / SDL_MIX_MAX_VOLUME) + 128)

void SDL_MixAudio (Uint8 *dst, const Uint8 *src, Uint32 len, int volume)
{
    Uint16 format;

    if(volume == 0) {
        return;
    }

    /* Mix the user-level audio format */
    if(mixer.audio_opened) {
        format = mixer.spec.format;
    } else {
        /* HACK HACK HACK */
        format = AUDIO_S16;
    }

    switch (format) {
        case AUDIO_U8: {
            Uint8 src_sample;

            while(len--) {
                src_sample = *src;
                ADJUST_VOLUME_U8(src_sample, volume);
                *dst = mix8[*dst + src_sample];
                ++dst;
                ++src;
            }
        }
        break;

        case AUDIO_S8: {
            Sint8 *dst8, *src8;
            Sint8 src_sample;
            int dst_sample;
            const int max_audioval = ((1 << (8 - 1)) - 1);
            const int min_audioval = -(1 << (8 - 1));

            src8 = (Sint8*)src;
            dst8 = (Sint8*)dst;
            while(len--) {
                src_sample = *src8;
                ADJUST_VOLUME(src_sample, volume);
                dst_sample = *dst8 + src_sample;
                if(dst_sample > max_audioval) {
                    *dst8 = max_audioval;
                } else if(dst_sample < min_audioval) {
                    *dst8 = min_audioval;
                } else {
                    *dst8 = dst_sample;
                }
                ++dst8;
                ++src8;
            }
        }
        break;

        case AUDIO_S16LSB: {
            Sint16 src_sample;
            int dst_sample;
            const int max_audioval = ((1 << (16 - 1)) - 1);
            const int min_audioval = -(1 << (16 - 1));

            len /= 2;
            while(len--) {
                src_sample = ((src[1] << 8) | src[0]);
                dst_sample = (int)src_sample;
                ADJUST_VOLUME(dst_sample, volume);
                src_sample = ((dst[1] << 8) | dst[0]);
                src += 2;
                dst_sample += (int)src_sample;
                if(dst_sample > max_audioval) {
                    dst_sample = max_audioval;
                } else if(dst_sample < min_audioval) {
                    dst_sample = min_audioval;
                }
                dst[0] = dst_sample & 0xFF;
                dst_sample >>= 8;
                dst[1] = dst_sample & 0xFF;
                dst += 2;
            }
        }
        break;

        case AUDIO_S16MSB: {
            Sint16 src_sample;
            int dst_sample;
            const int max_audioval = ((1 << (16 - 1)) - 1);
            const int min_audioval = -(1 << (16 - 1));

            len /= 2;
            while(len--) {
                src_sample = ((src[0] << 8) | src[1]);
                dst_sample = (int)src_sample;
                ADJUST_VOLUME(dst_sample, volume);
                src_sample = ((dst[0] << 8) | dst[1]);
                src += 2;
                dst_sample += (int)src_sample;
                if(dst_sample > max_audioval) {
                    dst_sample = max_audioval;
                } else if(dst_sample < min_audioval) {
                    dst_sample = min_audioval;
                }
                dst[1] = dst_sample & 0xFF;
                dst_sample >>= 8;
                dst[0] = dst_sample & 0xFF;
                dst += 2;
            }
        }
        break;

        default: /* If this happens... FIXME! */
            SDL_Log(ERROR, "unknown audio format");
            return;
    }
}

